浅谈AOP技术
一、前言
AOP(Aspect-Orient Program),意为面向切面编程,是通过预编译方式和运行时动态代理,实现程序功能统一维护的一种技术。通过AOP,可以降低业务代码各部分之间的耦合度。这么说可能有点抽象,我们可以想象一下某个场景:小明是某公司的程序员,一天老板给他定了一个APP的开发任务,他发现很多模块都需要写一段登录验证的功能,他想到自己曾学过AOP:只需要写一次登录验证的代码,再设置在项目中需要调用这段代码的地方,这样,编译期间登录验证的代码将会自动织入对应的代码逻辑处,高效率地完成了任务。
AOP技术常用于代码插桩(如下图效果1),也可以用来HOOK目标方法(如下图效果2)。当安全人员想要使用AOP技术实现某些安全功能时(比如对敏感API进行统一管控),往往需要考虑接入的开发人员是否也需要对业务代码作相应的修改。接下来我们将针对“对开发人员有无感知”方面,将AOP技术分为两类介绍,并在最后附上使用AOP技术管控Android系统敏感API的Demo。
切面(Aspect):被抽取的公共模块,由切点和增强组成。
连接点(Joinpoint):一个类或者一段代码拥有一些边界性质的特定点,比如调用前后、抛出异常时等。
切点(Pointcut):符合指定要求的连接点,可以被添加Advice。
增强(Advice):对切点的处理逻辑。
织入(Weaving):将增强添加到切点的过程。
这类AOP技术大多是在编译期间借助注解处理器或者方法拦截器向切点织入对应的增强逻辑,开发人员需要在业务代码中手动添加注解或者在应用时需要将方法拦截器和业务代码中的被代理类关联起来。以下介绍两种这类AOP技术:
1. Guice
Guice框架通过创建方法拦截器和某个接口绑定,可以对该接口满足特定条件的实现类进行切面处理。下图是一种简单编码实现方式:
经过AOP处理后的,代码逻辑示意图如下所示。其实现原理是在编译期间生成被代理类的子类,这个子类通过字节码修改,覆盖掉原来被切面拦截的类,并通过方法拦截器中编写的代码逻辑生成一个新的方法从而执行新方法。
Guice框架实现AOP的优势在于它不需要预先准备哪些类需要AOP,可以在后期直接通过方法拦截器绑定即可。缺点是它只能执行方法级别的AOP增强,并且方法和所在的类都不能被final修饰。
2. AspectJ
AspectJ是利用注解处理器在编译期间插入代码。其编码实现方法如下图所示。
常见用于织入的增强逻辑的注解如下:
@Around:全部覆盖被代理方法的增强逻辑
@Before:在被代理方法执行之前的增强逻辑
@After:在被代理方法执行之后的增强逻辑
@AfterThrowing:在被代理方法抛出异常后的增强逻辑
@AfterReturning:在被代理方法正常返回后的增强逻辑
在编译期间Aspect框架会自动织入切面类中定义的代码逻辑,完成AOP操作,具体代码执行流程如下图所示。
使用AspectJ框架实现AOP优势在于实现代码量比较小且难度相对简单,通过Aspect框架自带的注解识别切点、实现切面比较直观。缺点是由于它必须要借助注解被代理对象,所以不能管控系统层的方法。
这类AOP技术是在编译期间直接修改相关方法执行的字节码实现的。开发者引入打包好的AOP SDK,当在开发过程中调用到AOP管控到的接口的时候,就会自动执行AOP操作。
以下介绍两种常见的字节码修改框架及其实现AOP的方式。
1. Javassist
Javassisit主要通过在编译时ctMethod把增强逻辑织入到方法里,然后用ctClass把原来的类从ClassPool删掉,然后把新的有增强逻辑的类存入ClassPool中。下图是一种基于Javassist和Transform接口实现AOP的编码实现方法。
关于ctMethod中的一些重要方法:
insertBefore:在方法的起始位置插入代码。
insertAfter:在方法的所有return之前插入代码以确保语句能够被执行,除非遇到Exception。
InsertAt:在指定的位置插入代码。
SetBody:将方法的内容设置为要写入的代码,当方法被abstract修饰的时候,该修饰符被移除。
可以看得出来,用javassist修改字节码有一个优势就是不需要掌握晦涩的Dalvik字节码的相关知识,它是将要插入的代码当做字符串来插入的。这也说明了它有一个弊端:如果要插入的代码有逻辑错误的时候,在编译期间是发现不了的,只能运行时才能被发现。此外,Javassist还不支持JDK5以后的新语法。
使用javassist修改字节码框架代码逻辑示意图如下所示:
2. ASM
ASM是一个高性能,小体积的字节码修改框架,但是它需要开发者掌握字节码的相关知识,比如最基本的要知道Dalvik字节码的常见数据类型:
注:java类的类型用的是完整类名 比如一个String类型的数组 String[] 的字节码格式:[Ljava/lang/String;(类后面的分号不能省略) ;二维数组的表示:[[。以及要注意ASM对于要hook的方法类型不同,有不同的语法规定。
对于hook静态方法,切面中用于替换的方法的参数保持跟被代理方法一致。
对于hook成员方法,切面中用于替换的方法的第一个参数是被代理方法的Class对象,之后的参数跟原方法保持一致。
下图是ASM+Transform接口实现AOP的编码方式:
使用ASM字节码修改框架优势在于高性能且体积小,理论上可以实现任何关于字节码的修改,也就是说它可以通过hook系统层的方法对系统敏感API进行管控。但它也存在劣势,比如操作比较底层,不像Javassist那样直观地通过接口方法去插入增强逻辑,需要开发者了解字节码和JVM的相关知识,且需要手写过滤条件。其代码逻辑示意图如下图所示
3. 利用ASM修改字节码框架+Transform API 实现拦截恶意三方SDK获取敏感数据Demo
Transform可以在class文件编译成dex文件过程中做一些操作。在APK/AAR文件编译过程中,可以看成是一个个Transform Task执行,这些Transform Task有的是系统自带的,有的是自定义的;通过Gradle插件注册我们自定义的Transform。每个Transform的输出都将作为下一个Transform的输入,输入的内容有class文件,java文件等,我们就可以自定义一个或者多个Transform Task,并利用Transform相关的API以及字节码修改框架对输入的class文件做处理,从而对API进行管控。
Demo的实现思路以及最终实现结果如下图所示。本demo通过将Android系统敏感API作为被代理方法,在调用系统敏感API,比如getDeviceID的时候,可以选择地跳转到我们自定义的切面类的HookDeviceID的方法。在这基础上通过调用栈判断调用敏感API的来源是否是不可靠的三方SDK,如果是的话,则启用AOP,跳转到切面类的有关方法。
比如在测试APP里面编写获取IMEI的方法。
查看反编译代码,可以看到调用系统getDeviceID的地方已经替换成调用HookManager类的HookDeviceId方法:
本demo编写过程中核心代码是MethodVisitor中编写字节码修改的逻辑,以Hook系统的getDeviceId方法为例,首先查看系统层代码,发现android.telephony.TelephonyManager中的getDeviceId是一个成员方法:
替换的字节码修改逻辑:
由于系统的getDeviceID是成员方法,根据ASM Hook的规则,HookDeviceId方法的第一个参数必须是TelephonyManager类对象:
本文仅列举小部分实现AOP的框架,在开源社区还有很多优秀的开发者对这些原有的框架进行优化,比如AspectJ改造成更适用于Android的AspectJx。总之,关于AOP技术,其实就是将要统一处理的方法或者接口捆起来批量进行切面管理。在安全方面,常用于隐私接口管控、APP或快应用行为拦截或监控、数据埋点管理等;在开发角度来看,最常用于性能监控、代码结构优化等。当然,使用AOP可以使业务之间的耦合度降低,但也要考虑到相应的局限性。
参考文献
[1] Guice AOP(基础版):https://github.com/EdurtIO/learning-center-code/tree/master/guice/aop-basic
[2] AOP的专业术语:https://www.cnblogs.com/luler/p/14987266.html
[3] 使用Aspect实现AOP:https://blog.csdn.net/qq_28708411/article/details/112678230
[4] 基于javassist实现aop:https://cxymm.net/article/zhoujiaping123/91627991
[5] javassist使用全解析:https://www.cnblogs.com/rickiyang/p/11336268.html
[6] ASM hook隐私方法调用,防止App被下架:https://juejin.cn/post/7043399520486424612
[7] 蚂蚁安全平行切面白皮书:http://www.itstec.org.cn/aspect_oriented_security_white_paper.pdf
精彩文章推荐